import * as ts from "../../_namespaces/ts";
import * as fakes from "../../_namespaces/fakes";
import * as vfs from "../../_namespaces/vfs";

interface TestProjectSpecification {
    configFileName?: string;
    references?: readonly (string | ts.ProjectReference)[];
    files: { [fileName: string]: string };
    outputFiles?: { [fileName: string]: string };
    config?: object;
    options?: Partial<ts.CompilerOptions>;
}
interface TestSpecification {
    [path: string]: TestProjectSpecification;
}

function assertHasError(message: string, errors: readonly ts.Diagnostic[], diag: ts.DiagnosticMessage) {
    if (!errors.some(e => e.code === diag.code)) {
        const errorString = errors.map(e => `    ${e.file ? e.file.fileName : "[global]"}: ${e.messageText}`).join("\r\n");
        assert(false, `${message}: Did not find any diagnostic for ${diag.message} in:\r\n${errorString}`);
    }
}

function assertNoErrors(message: string, errors: readonly ts.Diagnostic[]) {
    if (errors && errors.length > 0) {
        assert(false, `${message}: Expected no errors, but found:\r\n${errors.map(e => `    ${e.messageText}`).join("\r\n")}`);
    }
}

function combineAllPaths(...paths: string[]) {
    let result = paths[0];
    for (let i = 1; i < paths.length; i++) {
        result = ts.combinePaths(result, paths[i]);
    }
    return result;
}

const emptyModule = "export { };";

/**
 * Produces the text of a source file which imports all of the
 * specified module names
 */
function moduleImporting(...names: string[]) {
    return names.map((n, i) => `import * as mod_${i} from ${n}`).join("\r\n");
}

function testProjectReferences(spec: TestSpecification, entryPointConfigFileName: string, checkResult: (prog: ts.Program, host: fakes.CompilerHost) => void) {
    const files = new ts.Map<string, string>();
    for (const key in spec) {
        const sp = spec[key];
        const configFileName = combineAllPaths("/", key, sp.configFileName || "tsconfig.json");
        const options = {
            compilerOptions: {
                composite: true,
                outDir: "bin",
                ...sp.options
            },
            references: (sp.references || []).map(r => {
                if (typeof r === "string") {
                    return { path: r };
                }
                return r;
            }),
            ...sp.config
        };
        const configContent = JSON.stringify(options);
        const outDir = options.compilerOptions.outDir;
        files.set(configFileName, configContent);
        for (const sourceFile of Object.keys(sp.files)) {
            files.set(sourceFile, sp.files[sourceFile]);
        }
        if (sp.outputFiles) {
            for (const outFile of Object.keys(sp.outputFiles)) {
                files.set(combineAllPaths("/", key, outDir, outFile), sp.outputFiles[outFile]);
            }
        }
    }

    const vfsys = new vfs.FileSystem(false, { files: { "/lib.d.ts": ts.TestFSWithWatch.libFile.content } });
    files.forEach((v, k) => {
        vfsys.mkdirpSync(ts.getDirectoryPath(k));
        vfsys.writeFileSync(k, v);
    });
    const host = new fakes.CompilerHost(new fakes.System(vfsys));

    const { config, error } = ts.readConfigFile(entryPointConfigFileName, name => host.readFile(name));

    // We shouldn't have any errors about invalid tsconfig files in these tests
    assert(config && !error, ts.flattenDiagnosticMessageText(error && error.messageText, "\n"));
    const file = ts.parseJsonConfigFileContent(config, ts.parseConfigHostFromCompilerHostLike(host), ts.getDirectoryPath(entryPointConfigFileName), {}, entryPointConfigFileName);
    file.options.configFilePath = entryPointConfigFileName;
    const prog = ts.createProgram({
        rootNames: file.fileNames,
        options: file.options,
        host,
        projectReferences: file.projectReferences
    });
    checkResult(prog, host);
}

describe("unittests:: config:: project-references meta check", () => {
    it("default setup was created correctly", () => {
        const spec: TestSpecification = {
            "/primary": {
                files: { "/primary/a.ts": emptyModule },
                references: []
            },
            "/reference": {
                files: { "/secondary/b.ts": moduleImporting("../primary/a") },
                references: ["../primary"]
            }
        };
        testProjectReferences(spec, "/primary/tsconfig.json", prog => {
            assert.isTrue(!!prog, "Program should exist");
            assertNoErrors("Sanity check should not produce errors", prog.getOptionsDiagnostics());
        });
    });
});

/**
 * Validate that we enforce the basic settings constraints for referenced projects
 */
describe("unittests:: config:: project-references constraint checking for settings", () => {
    it("errors when declaration = false", () => {
        const spec: TestSpecification = {
            "/primary": {
                files: { "/primary/a.ts": emptyModule },
                references: [],
                options: {
                    declaration: false
                }
            }
        };

        testProjectReferences(spec, "/primary/tsconfig.json", program => {
            const errs = program.getOptionsDiagnostics();
            assertHasError("Reports an error about the wrong decl setting", errs, ts.Diagnostics.Composite_projects_may_not_disable_declaration_emit);
        });
    });

    it("errors when the referenced project doesn't have composite:true", () => {
        const spec: TestSpecification = {
            "/primary": {
                files: { "/primary/a.ts": emptyModule },
                references: [],
                options: {
                    composite: false
                }
            },
            "/reference": {
                files: { "/secondary/b.ts": moduleImporting("../primary/a") },
                references: ["../primary"],
                config: {
                    files: ["b.ts"]
                }
            }
        };
        testProjectReferences(spec, "/reference/tsconfig.json", program => {
            const errs = program.getOptionsDiagnostics();
            assertHasError("Reports an error about 'composite' not being set", errs, ts.Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true);
        });
    });

    it("does not error when the referenced project doesn't have composite:true if its a container project", () => {
        const spec: TestSpecification = {
            "/primary": {
                files: { "/primary/a.ts": emptyModule },
                references: [],
                options: {
                    composite: false
                }
            },
            "/reference": {
                files: { "/secondary/b.ts": moduleImporting("../primary/a") },
                references: ["../primary"],
            }
        };
        testProjectReferences(spec, "/reference/tsconfig.json", program => {
            const errs = program.getOptionsDiagnostics();
            assertNoErrors("Reports an error about 'composite' not being set", errs);
        });
    });

    it("errors when the file list is not exhaustive", () => {
        const spec: TestSpecification = {
            "/primary": {
                files: {
                    "/primary/a.ts": "import * as b from './b'",
                    "/primary/b.ts": "export {}"
                },
                config: {
                    files: ["a.ts"]
                }
            }
        };

        testProjectReferences(spec, "/primary/tsconfig.json", program => {
            const errs = program.getSemanticDiagnostics(program.getSourceFile("/primary/a.ts"));
            assertHasError("Reports an error about b.ts not being in the list", errs, ts.Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern);
        });
    });

    it("errors when the referenced project doesn't exist", () => {
        const spec: TestSpecification = {
            "/primary": {
                files: { "/primary/a.ts": emptyModule },
                references: ["../foo"]
            }
        };
        testProjectReferences(spec, "/primary/tsconfig.json", program => {
            const errs = program.getOptionsDiagnostics();
            assertHasError("Reports an error about a missing file", errs, ts.Diagnostics.File_0_not_found);
        });
    });

    it("errors when a prepended project reference doesn't set outFile", () => {
        const spec: TestSpecification = {
            "/primary": {
                files: { "/primary/a.ts": emptyModule },
                references: [{ path: "../someProj", prepend: true }]
            },
            "/someProj": {
                files: { "/someProj/b.ts": "const x = 100;" }
            }
        };
        testProjectReferences(spec, "/primary/tsconfig.json", program => {
            const errs = program.getOptionsDiagnostics();
            assertHasError("Reports an error about outFile not being set", errs, ts.Diagnostics.Cannot_prepend_project_0_because_it_does_not_have_outFile_set);
        });
    });

    it("errors when a prepended project reference output doesn't exist", () => {
        const spec: TestSpecification = {
            "/primary": {
                files: { "/primary/a.ts": "const y = x;" },
                references: [{ path: "../someProj", prepend: true }]
            },
            "/someProj": {
                files: { "/someProj/b.ts": "const x = 100;" },
                options: { outFile: "foo.js" }
            }
        };
        testProjectReferences(spec, "/primary/tsconfig.json", program => {
            const errs = program.getOptionsDiagnostics();
            assertHasError("Reports an error about outFile being missing", errs, ts.Diagnostics.Output_file_0_from_project_1_does_not_exist);
        });
    });
});

/**
 * Path mapping behavior
 */
describe("unittests:: config:: project-references path mapping", () => {
    it("redirects to the output .d.ts file", () => {
        const spec: TestSpecification = {
            "/alpha": {
                files: { "/alpha/a.ts": "export const m: number = 3;" },
                references: [],
                outputFiles: { "a.d.ts": emptyModule }
            },
            "/beta": {
                files: { "/beta/b.ts": "import { m } from '../alpha/a'" },
                references: ["../alpha"]
            }
        };
        testProjectReferences(spec, "/beta/tsconfig.json", program => {
            assertNoErrors("File setup should be correct", program.getOptionsDiagnostics());
            assertHasError("Found a type error", program.getSemanticDiagnostics(), ts.Diagnostics.Module_0_has_no_exported_member_1);
        });
    });
});

describe("unittests:: config:: project-references nice-behavior", () => {
    it("issues a nice error when the input file is missing", () => {
        const spec: TestSpecification = {
            "/alpha": {
                files: { "/alpha/a.ts": "export const m: number = 3;" },
                references: []
            },
            "/beta": {
                files: { "/beta/b.ts": "import { m } from '../alpha/a'" },
                references: ["../alpha"]
            }
        };
        testProjectReferences(spec, "/beta/tsconfig.json", program => {
            assertHasError("Issues a useful error", program.getSemanticDiagnostics(), ts.Diagnostics.Output_file_0_has_not_been_built_from_source_file_1);
        });
    });

    it("issues a nice error when the input file is missing when module reference is not relative", () => {
        const spec: TestSpecification = {
            "/alpha": {
                files: { "/alpha/a.ts": "export const m: number = 3;" },
                references: []
            },
            "/beta": {
                files: { "/beta/b.ts": "import { m } from '@alpha/a'" },
                references: ["../alpha"],
                options: {
                    baseUrl: "./",
                    paths: {
                        "@alpha/*": ["/alpha/*"]
                    }
                }
            }
        };
        testProjectReferences(spec, "/beta/tsconfig.json", program => {
            assertHasError("Issues a useful error", program.getSemanticDiagnostics(), ts.Diagnostics.Output_file_0_has_not_been_built_from_source_file_1);
        });
    });
});

/**
 * 'composite' behavior
 */
describe("unittests:: config:: project-references behavior changes under composite: true", () => {
    it("doesn't infer the rootDir from source paths", () => {
        const spec: TestSpecification = {
            "/alpha": {
                files: { "/alpha/src/a.ts": "export const m: number = 3;" },
                options: {
                    declaration: true,
                    outDir: "bin"
                },
                references: []
            }
        };
        testProjectReferences(spec, "/alpha/tsconfig.json", (program, host) => {
            program.emit();
            assert.deepEqual(host.outputs.map(e => e.file).sort(), ["/alpha/bin/src/a.d.ts", "/alpha/bin/src/a.js", "/alpha/bin/tsconfig.tsbuildinfo"]);
        });
    });
});

describe("unittests:: config:: project-references errors when a file in a composite project occurs outside the root", () => {
    it("Errors when a file is outside the rootdir", () => {
        const spec: TestSpecification = {
            "/alpha": {
                files: { "/alpha/src/a.ts": "import * from '../../beta/b'", "/beta/b.ts": "export { }" },
                options: {
                    declaration: true,
                    outDir: "bin"
                },
                references: []
            }
        };
        testProjectReferences(spec, "/alpha/tsconfig.json", (program) => {
            const semanticDiagnostics = program.getSemanticDiagnostics(program.getSourceFile("/alpha/src/a.ts"));
            assertHasError("Issues an error about the rootDir", semanticDiagnostics, ts.Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files);
            assertHasError("Issues an error about the fileList", semanticDiagnostics, ts.Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern);
        });
    });
});
